/*******************************************************************************
 * Copyright (c) 2001, 2002 Boris Pruessmann and others. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Common Public License v0.5 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/cpl-v05.html
 * 
 * Contributors: Boris Pruessmann - Initial implementation.
 ******************************************************************************/
package net.sourceforge.perforce.ui;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import net.sourceforge.perforce.core.IResourceStateChangeListener;
import net.sourceforge.perforce.core.PerforceCorePlugin;
import net.sourceforge.perforce.core.api.IPerforceServer;
import net.sourceforge.perforce.core.internal.P4TeamProvider;
import net.sourceforge.perforce.core.resources.IChangelist;
import net.sourceforge.perforce.core.resources.LocalFile;
import net.sourceforge.perforce.core.resources.LocalFolder;
import net.sourceforge.perforce.core.resources.LocalResource;
import net.sourceforge.perforce.core.syncinfo.FolderSyncInfo;
import net.sourceforge.perforce.core.syncinfo.IResourceStateDelta;
import net.sourceforge.perforce.core.syncinfo.ResourceSyncInfo;
import net.sourceforge.perforce.ui.internal.Policy;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ILabelDecorator;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.team.core.RepositoryProvider;
import org.eclipse.ui.IDecoratorManager;

import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * @version $Revision: 488 $
 * @author <a href="mailto:bpruessmann@users.sourceforge.net">Boris
 *         Pruessmann</a>
 */
public class PerforceDecorator extends LabelProvider implements ILabelDecorator,
    IResourceStateChangeListener {
  // --------------------------------------------------------------------
  // Private Static Variables

  /** Stores the singleton instance of the decorator. */
  private static PerforceDecorator instance;

  // ---------------------------------------------------------------------------
  // Private Variables

  /** The OverlayIconCache. */
  private OverlayIconCache iconCache = new OverlayIconCache();

  @SuppressWarnings("unused")
  private DecorationSettings preferenceSettings;

  // -------------------------------------------------------------------------
  // Public Constructors

  /** Constructor for PerforceDecorator. */
  public PerforceDecorator() {
    Policy.assertNull(instance);
    instance = this;

    preferenceSettings = new DecorationSettings();

    PerforceCorePlugin.getPlugin().addResourceStateChangeListener(this);
  }

  // -----------------------------------------------------------------------
  // Public Static Methods

  /** Call this method to shutdown the decorator. */
  public static void shutdown() {
    if (instance != null) {
      instance.dispose();
    }
  }

  /** Call this method to refresh the decorator (drop cache, redraw). */
  public static void refresh() {
    PerforceDecorator activeDecorator = getActiveP4Decorator();

    if (activeDecorator == null) {
      return;
    }

    activeDecorator.fireLabelProviderChanged(new LabelProviderChangedEvent(activeDecorator));
  }


  //----------------------------------------------------------------------------
  // -- Public Methods

  public void resourceStateChanged(IResourceStateDelta[] resourceStateDeltas) {
    List<LabelProviderChangedEvent> events = Lists.newArrayList();
    for (int i = 0, length = resourceStateDeltas.length; i < length; i++) {
      if (resourceStateDeltas[i].getKind() != IResourceStateDelta.REMOVED)
        events.add(new LabelProviderChangedEvent(this, resourceStateDeltas[i].getResource()));
    }
    postLabelEvents(events.toArray(new LabelProviderChangedEvent[0]));
  }

  public void changelistStateChanged(IChangelist[] changelists) {
  }

  public void changelistStateChanged(IPerforceServer[] servers) {
  }

  /** @see ILabelDecorator#decorateImage(Image, Object) */
  public Image decorateImage(Image image, Object element) {
    IResource resource = getResource(element);
    if (resource == null || image == null || resource.getType() == IResource.ROOT) {
      return image;
    }

    if (!checkProvider(resource)) return image;

    boolean isDirty = getDirtyFlag(resource);

    PerforceDecoration decoration = computeOverlays(isDirty, resource);

    List<ImageDescriptor> overlays = decoration.getOverlays();
    int[] locations = decoration.getLocations();
    if (overlays != null) {
      return iconCache.getImageFor(new DecoratorOverlayIcon(image, overlays
          .toArray(new ImageDescriptor[overlays.size()]), locations));
    }

    return image;
  }

  /** @see ILabelDecorator#decorateText(String, Object) */
  public String decorateText(String text, Object element) {
    IResource resource = getResource(element);
    if (resource == null || text == null || resource.getType() == IResource.ROOT) return text;

    String format = P4DecoratorConfiguration.getFormat(resource);
    if (format == null) return text;

    if (!checkProvider(resource)) return text;

    boolean isDirty = getDirtyFlag(resource);

    Map<String, String> bindings = computeDecoratedText(isDirty, resource);
    if (bindings == null || bindings.isEmpty()) {
      return text;
    }

    bindings.put(P4DecoratorConfiguration.RESOURCE_NAME, text);

    return P4DecoratorConfiguration.bind(format, bindings);
  }

  private boolean getDirtyFlag(IResource resource) {
    IPreferenceStore store = PerforceUIPlugin.getPlugin().getPreferenceStore();
    boolean isDirty = false;
    boolean computeDeepDirtyCheck = store.getBoolean(PerforceUIPlugin.CALCULATE_DIRTY_PREFERENCE);
    if (resource.getType() == IResource.FILE || computeDeepDirtyCheck) {
      isDirty = isDirty(resource);
    }
    return isDirty;
  }

  private boolean checkProvider(IResource resource) {
    RepositoryProvider provider = RepositoryProvider.getProvider(resource.getProject());
    if (!resource.exists() || provider == null
        || !provider.getID().equals(PerforceCorePlugin.getTypeId())) return false;

    P4TeamProvider teamProvider = (P4TeamProvider) provider;
    if (teamProvider.isIgnored(resource)) return false;
    return true;
  }

  @Override
  public void dispose() {
    super.dispose();

    PerforceCorePlugin.getPlugin().removeResourceStateChangeListener(this);

    iconCache.disposeAll();
    instance = null;
  }

  // ----------------------------------------------------------------------
  // Private Static Methods

  /**
   * Call this method to retrieve the currently active PerforceDecorator.
   * 
   * @return the active perforce decorator.
   */
  private static PerforceDecorator getActiveP4Decorator() {
    IDecoratorManager manager = PerforceUIPlugin.getPlugin().getWorkbench().getDecoratorManager();

    if (manager.getEnabled("net.sourceforge.perforce.ui.decorator")) {
      return (PerforceDecorator) manager.getLabelDecorator("net.sourceforge.perforce.ui.decorator");
    }

    return null;
  }

  //----------------------------------------------------------------------------
  // - Private Methods

  private IResource getResource(Object object) {
    if (object instanceof IResource) {
      return (IResource) object;
    } else if (object instanceof IAdaptable) {
      return (IResource) ((IAdaptable) object).getAdapter(IResource.class);
    } else {
      return null;
    }
  }

  /**
   * Called to fire label events to notify the views about changed decorations.
   * 
   * @param events the events to fire.
   */
  private void postLabelEvents(final LabelProviderChangedEvent[] events) {
    // now post the change events to the UI thread
    if (events.length > 0) {
      Display.getDefault().asyncExec(new Runnable() {
        public void run() {
          for (int i = 0; i < events.length; i++) {
            fireLabelProviderChanged(events[i]);
          }
        }
      });
    }
  }

  // ----------------------------------------------------------------------
  // Private Nested Classes

  /**
   * Calculates the dirty state of the given resource.
   * 
   * @param resource the resource.
   * @return true if resource is dirty, false otherwise.
   */
  public boolean isDirty(IResource resource) {
    final CoreException decoratorException =
        new CoreException(new Status(IStatus.OK, "id", 1, "", null));//$NON-NLS-1$ //$NON-NLS-2$
    try {
      resource.accept(new IResourceVisitor() {
        public boolean visit(IResource resource) throws CoreException {

          // a project can't be dirty, continue with its children
          if (resource.getType() == IResource.PROJECT) {
            return true;
          }

          // if the resource does not exist in the workbench or on the
          // file system, stop searching.
          if (!resource.exists()) {
            return false;
          }

          LocalResource localResource = LocalResource.createFromResource(resource);
          if (localResource.isIgnored()) {
            return false;
          }

          if (localResource instanceof LocalFile) {
            if (localResource.isDirty()) {
              throw decoratorException;
            }
          }

          return true;
        }
      }, IResource.DEPTH_INFINITE, true);
    } catch (CoreException e) {
      // if our exception was caught, we know there's a dirty child
      return e == decoratorException;
    }

    return false;
  }

  /**
   * Computes the graphical decorations.
   * 
   * @param isDirty forced dirty flag.
   * @param resource the resource.
   */
  public PerforceDecoration computeOverlays(boolean isDirty, IResource resource) {
    PerforceDecoration decoration = new PerforceDecoration();
    List<ImageDescriptor> overlays = Lists.newArrayList();
    List<Integer> locations = Lists.newArrayList();

    switch (resource.getType()) {
      case IResource.PROJECT:
        overlays.add(OverlayIconCache.HAS_REMOTE);
        locations.add(Integer.valueOf(OverlayIconCache.HAS_REMOTE_LOCATION));
        break;

      case IResource.FOLDER: {
        LocalResource localResource = LocalResource.createFromResource(resource);
        if (localResource == null) {
          return new PerforceDecoration();
        }

        if (localResource.hasRemote() && DecorationSettings.indicateHasRemote) {
          overlays.add(OverlayIconCache.HAS_REMOTE);
          locations.add(Integer.valueOf(OverlayIconCache.HAS_REMOTE_LOCATION));
        }
      }
        break;

      case IResource.FILE: {
        LocalFile localFile = (LocalFile) LocalResource.createFromResource(resource);
        if (localFile == null) {
          return new PerforceDecoration();
        }

        if (!localFile.hasRemote() && DecorationSettings.indicateNewResource) {
          overlays.add(OverlayIconCache.NO_REMOTE);
          locations.add(Integer.valueOf(OverlayIconCache.NO_REMOTE_LOCATION));
        } else if (localFile.hasRemote()) {
          if (localFile.isOutOfDate()) {
            overlays.add(OverlayIconCache.OUT_OF_DATE);
            locations.add(Integer.valueOf(OverlayIconCache.OUT_OF_DATE_LOCATION));
          } else if (DecorationSettings.indicateHasRemote) {
            overlays.add(OverlayIconCache.HAS_REMOTE);
            locations.add(Integer.valueOf(OverlayIconCache.HAS_REMOTE_LOCATION));
          }
        }

        ResourceSyncInfo resourceSyncInfo = localFile.getSyncInfo();
        if (!resourceSyncInfo.isNull()) {
          if (resourceSyncInfo.isOtherOpen() && DecorationSettings.indicateOpenedByOther) {
            overlays.add(OverlayIconCache.OTHER_OPEN_FOR_EDIT);
            locations.add(Integer.valueOf(OverlayIconCache.OTHER_OPEN_FOR_EDIT_LOCATION));
          }

          if (resourceSyncInfo.hasOurLock()) {
            overlays.add(OverlayIconCache.OUR_LOCK);
            locations.add(Integer.valueOf(OverlayIconCache.OUR_LOCK_LOCATION));
          }

          if (resourceSyncInfo.isDeleted()) {
            overlays = Lists.newArrayList();
            locations = Lists.newArrayList();
          } else if (DecorationSettings.indicateOpenedForAdd
              && (resourceSyncInfo.isAdded() || resourceSyncInfo.isBranched())) {
            overlays.add(OverlayIconCache.OPEN_FOR_ADD);
            locations.add(Integer.valueOf(OverlayIconCache.OPEN_FOR_ADD_LOCATION));
          } else if (DecorationSettings.indicateOpenedForEdit && resourceSyncInfo.isEdited()) {
            overlays.add(OverlayIconCache.OPEN_FOR_EDIT);
            locations.add(Integer.valueOf(OverlayIconCache.OPEN_FOR_EDIT_LOCATION));
          }
        }
      }
        break;
    }

    if (!overlays.isEmpty()) {
      Integer[] integers = locations.toArray(new Integer[locations.size()]);
      int[] ints = new int[integers.length];
      for (int i = 0; i < integers.length; i++) {
        ints[i] = integers[i].intValue();
      }
      decoration.setLocations(ints);
      decoration.setOverlays(overlays);
    }
    return decoration;
  }

  /**
   * Computes the text decorations.
   * 
   * @param isDirty forced dirty flag.
   * @param resource the resource.
   * @return the PerforceDecoratin object.
   */
  public Map<String, String> computeDecoratedText(boolean isDirty, IResource resource) {
    IPreferenceStore store = PerforceUIPlugin.getPlugin().getPreferenceStore();
    Map<String, String> bindings = Maps.newHashMapWithExpectedSize(2);

    if (isDirty) {
      bindings.put(P4DecoratorConfiguration.DIRTY_FLAG, store
          .getString(IPerforceUIConstants.PREF_DIRTY_FLAG));
    }

    if (resource.getType() != IResource.FILE) {
      LocalFolder folder = (LocalFolder) LocalResource.createFromResource(resource);
      if (folder == null) {
        return Collections.emptyMap();
      }

      FolderSyncInfo syncInfo = folder.getFolderSync();
      if (syncInfo != null && syncInfo.isAdded()) {
        bindings.put(P4DecoratorConfiguration.DIRTY_FLAG, store
            .getString(IPerforceUIConstants.PREF_ADDED_FLAG));
      }

      if (resource.getType() == IResource.PROJECT) {
        IPerforceServer server = folder.getServer();

        bindings.put(P4DecoratorConfiguration.P4PORT, server.getPort());
        bindings.put(P4DecoratorConfiguration.P4CLIENT, server.getClient());
        bindings.put(P4DecoratorConfiguration.P4USER, server.getUser());
      }
    } else {
      LocalFile localFile = (LocalFile) LocalResource.createFromResource(resource);
      if (localFile == null) {
        return Collections.emptyMap();
      }

      ResourceSyncInfo resourceSyncInfo = localFile.getSyncInfo();
      if (!resourceSyncInfo.isNull()) {
        if (resourceSyncInfo.isDeleted()) {
          return Collections.emptyMap();
        } else if (resourceSyncInfo.isAdded() || resourceSyncInfo.isBranched()) {
          bindings.put(P4DecoratorConfiguration.DIRTY_FLAG, store
              .getString(IPerforceUIConstants.PREF_ADDED_FLAG));
        } else {
          bindings.put(P4DecoratorConfiguration.HAVE_REV, "#" + resourceSyncInfo.getHaveRev());

          bindings.put(P4DecoratorConfiguration.HEAD_REV, "#" + resourceSyncInfo.getHeadRev());
        }
      }
    }

    return bindings;
  }

  /** The decorator's OverlayIcon. */
  private static class DecoratorOverlayIcon extends OverlayIcon {
    // ---------------------------------------------------------------------
    // Public Constructors

    /**
     * Constructor for DecoratorOverlayIcon.
     * 
     * @param base the base iamge.
     * @param overlays the overlay to apply.
     * @param locations the locations of the overlays.
     */
    public DecoratorOverlayIcon(Image base, ImageDescriptor[] overlays, int[] locations) {
      super(base, overlays, locations, new Point(base.getBounds().width, base.getBounds().height));
    }

    // -----------------------------------------------------------------------
    // Protected Methods

    @Override
    protected void drawOverlays(ImageDescriptor[] overlays, int[] locations) {
      Point size = getSize();
      for (int i = overlays.length - 1; i >= 0; --i) {
        ImageData imageData = overlays[i].getImageData();

        switch (locations[i]) {
          case TOP_LEFT:
            drawImage(imageData, 0, 0);
            break;

          case TOP_RIGHT:
            drawImage(imageData, size.x - imageData.width, 0);
            break;

          case BOTTOM_LEFT:
            drawImage(imageData, 0, size.y - imageData.height);
            break;

          case BOTTOM_RIGHT:
            drawImage(imageData, size.x - imageData.width, size.y - imageData.height);
            break;
        }
      }
    }
  }
}
